﻿class WebView2 {
    static COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND := {DENY: 0, ALLOW: 1, DENY_CORS: 2}
    
    wv := false
    controller := false
    controllerCreatedCallbacks := []
    
    __new(parent, dllPath := "WebView2Loader.dll") {
        if (Type(parent) != "Gui") {
            throw Exception("Parent must be of type: Gui")
        }
        
        this.parent := parent
        this.dllPath := dllPath
        this.controllerCompletedHandler := ICoreWebView2CreateCoreWebView2ControllerCompletedHandler(this)
        this.envCompletedHandler := IWebView2CreateWebView2EnvironmentCompletedHandler(this.parent, this.controllerCompletedHandler)
    }
    
    OnControllerCompleted(cb) {
        if (!cb.HasMethod("Call")) {
            throw TypeError(A_ThisFunc ":: Parameter must be callable.")
        }
        this.controllerCreatedCallbacks.push(cb)
    }
    
    /**
     * Adds a callback to be called when navigation starts in the browser.
     * Use the returned token to remove the handler with <WebView>.removeNavigationStarting()
     */
    OnNavigationStarting(cb) {
        if (!cb.HasMethod("Call")) {
            throw Exception(A_ThisFunc ":: Parameter must be callable.")
        }
        
        ; ICoreWebView2::add_navigationstarting
        ComCall(7, this.coreWebView, "Ptr", ICoreWebView2NavigationStartingEventHandler(this, cb), "Int64*", &token := 0)
        
        return token
    }
    
    removeNavigationStarting(token) {
        return !ComCall(8, this.coreWebView, "Int64", token)
    }
    
    OnNavigationCompleted(cb) {
        if (!cb.HasMethod("Call")) {
            throw Exception(A_ThisFunc ":: Parameter must be callable.")
        }
        
        ; ICoreWebView2::add_navigationCompleted
        ComCall(15, this.coreWebView, "Ptr", ICoreWebView2NavigationCompletedEventHandler(this, cb), "Int64*", &token := 0)
        
        return token
    }
    
    removeNavigationCompleted(token) {
        return !ComCall(16, this.coreWebView, "Int64", token)
    }
    
    isVisible {
        ; IWebView2WebViewController::get_IsVisible
        get {
            ComCall(3, this.controller, "Int*", &isVisible := 0)
            return isVisible
        }
        
        ; IWebView2WebViewController::put_IsVisible
        set {
            ComCall(4, this.controller, "Int", Integer(value))
        }
    }
    
    uri {
        ; ICoreWebView2::get_Source
        get {
            ComCall(4, this.coreWebView, "Str*", &uri := "")
            return uri
        }
    }
    
    IsSuspended {
        ; ICoreWebView2::get_IsSuspended
        get {
            ComCall(70, this.coreWebView, "Int*", &isSuspended := 0)
            return isSuspended
        }
    }
    
    ; IWebView2WebViewController::get_Bounds
    get_Bounds() {
        local RECT := BufferAlloc(16)
        
        ComCall(5, this.controller, "Ptr", RECT)
        
        return {
            x: NumGet(RECT, "Int"),
            y: NumGet(RECT, 4, "Int"),
            w: NumGet(RECT, 8, "Int") - NumGet(RECT, "Int"),
            h: NumGet(RECT, 12, "Int") - NumGet(RECT, 4, "Int")
        }
    }
    
    ; Resize WebView to fit the bounds of the parent window.
    ; IWebView2WebViewController::put_Bounds Method.
    ; https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/0-9-538/icorewebview2controller#put_bounds
    put_Bounds(x := unset, y := unset, w := unset, h := unset) {
        local RECT := BufferAlloc(16)
        
        if (!IsSet(x) && !IsSet(y) && !IsSet(w) && !IsSet(h)) {
            DllCall("User32.dll\GetClientRect", "Ptr", this.parent.hWnd, "Ptr", RECT := BufferAlloc(16))
        }
        else {
            NumPut("Int", IsSet(x) ? x : 0, "Int", IsSet(y) ? y : 0, "Int", IsSet(w) ? w : 0, "Int", IsSet(h) ? h : 0, RECT)
        }
        
        ComCall(6, this.controller, "Ptr", RECT)
    }
    
    addMoveFocusRequested(cb) {
        ComCall(13, this.controller, "Ptr", ICoreWebView2MoveFocusRequestedEventHandler(this, cb), "Int64*", &token := 0)
        return (() => ComCall(14, this.controller, "Int64", token))
    }
    
    addLostFocus(cb) {
        ComCall(17, this.controller, "Ptr", ICoreWebView2FocusChangedEventHandler(this, cb), "Int64*", &token := 0)
        return (() => ComCall(18, this.controller, "Int64", token))
    }
    
    NotifyParentWinPosChanged() {
        ComCall(23, this.controller)
    }
    
    ; ICoreWebView2::Navigate
    navigate(url) {
        return !ComCall(5, this.coreWebView, "Str", url)
    }
    
    navigateToString(html) {
        return !ComCall(6, this.coreWebView, "Str", html)
    }
    
    AddHostObjectToScript(name, obj) {
        ; cObj := ComObject(9, ObjPtr(obj))
        ComCall(49, this.coreWebView, "Str", name, "Ptr", obj)
    }
    
    OpenDevToolsWindow() {
        return !ComCall(51, this.coreWebView)
    }
    
    TrySuspend(cb) {
        return !ComCall(68, this.coreWebView, "Ptr", ICoreWebView2TrySuspendCompletedHandler(this.coreWebView, cb))
    }
    
    SetVirtualHostNameToFolderMapping(hostName, folderPath, accessKind := false) {
        accessKind := accessKind && WebView2.COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND.HasOwnProp(accessKind) ? WebView2.COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND.%accessKind% : WebView2.COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND.DENY
        return !ComCall(71, this.coreWebView, "Str", hostName, "Str", folderPath, "Int", 0)
    }
    
    ExecuteScript(script, cb := 0) {
        if (cb != 0 && !cb.HasMethod("Call")) {
            throw Exception(A_ThisFunc ":: Parameter must be callable.")
        }
        ComCall(29, this.coreWebView, "Str", script, "Ptr", cb ? ICoreWebView2ExecuteScriptCompletedHandler(this, cb) : cb)
    }
    
    create() {
        if (R := DllCall(this.dllPath . "\CreateCoreWebView2Environment", "Ptr", this.envCompletedHandler, "UInt")) {
            MsgBox("ERROR " . Format("{:08X}", R))
        }
    }
}

class ICoreWebView2MoveFocusRequestedEventHandler extends IUnknown {
    __New(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, hresult, args) {
        this.cb.call(this.wv, ICoreWebView2MoveFocusRequestedEventArgs(args))
    }
}

class ICoreWebView2MoveFocusRequestedEventArgs {
    static MOVE_REASON := ["COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC", "COREWEBVIEW2_MOVE_FOCUS_REASON_NEXT", "COREWEBVIEW2_MOVE_FOCUS_REASON_PREVIOUS"]
    __New(pEventArgs) {
        this.pEventArgs := pEventArgs
    }
    
    reason {
        get {
            ComCall(3, this.pEventArgs, "Int*", &reason := 0)
            return ICoreWebView2MoveFocusRequestedEventArgs.MOVE_REASON[reason + 1]
        }
    }
    
    isHandled {
        get {
            ComCall(4, this.pEventArgs, "Int*", &handled := 0)
            return handled
        }
        set {
            ComCall(5, this.pEventArgs, "Int", Integer(value))
        }
    }
}


class ICoreWebView2FocusChangedEventHandler extends IUnknown {
    __New(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, hresult, args) {
        this.cb.call(this.wv)
    }
}

class ICoreWebView2TrySuspendCompletedHandler extends IUnknown {
    __New(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, hresult, isSuccessful) {
        this.cb.call(isSuccessful)
    }
}

class IWebView2CreateWebView2EnvironmentCompletedHandler extends IUnknown {
    __New(parent, controllerCompleted) {
        super.__new()
        
        this.parent := parent
        this.controllerCompleted := controllerCompleted
    }
    
    Invoke(thisPtr, hresult, ICoreWebView2Env) {
        ; ICoreWebView2Environment::CreateCoreWebView2Controller Method.
        ; https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/0-9-538/icorewebview2environment#createcorewebview2controller
        return ComCall(3, ICoreWebView2Env, "Ptr", this.parent.hwnd, "Ptr", this.controllerCompleted)
    }
}

class ICoreWebView2CreateCoreWebView2ControllerCompletedHandler extends IUnknown {
    __new(wv) {
        super.__new()
        
        this.wv := wv
    }
    
    Invoke(thisPtr, HRESULT, IWebView2WebViewController) {
        this.wv.controller := IWebView2WebViewController
        ObjAddRef(IWebView2WebViewController) ; This was key to retain a reference to the Controller
        
        ; IWebView2WebViewController::get_CoreWebView2
        ; https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/0-9-538/icorewebview2controller#get_corewebview2
        ComCall(25, IWebView2WebViewController, "Ptr*", &coreWebView := 0)
        this.wv.coreWebView := coreWebView
        
        for cb in this.wv.controllerCreatedCallbacks {
            cb.call(this.wv)
        }

        return 0  ; S_OK.
    }
}

class ICoreWebView2ExecuteScriptCompletedHandler extends IUnknown {
    __new(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, hresult, result) {
        this.cb.call(this.wv, StrGet(result))
    }
}

class ICoreWebView2NavigationStartingEventHandler extends IUnknown {
    __new(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, IWebView2WebView, pEventArgs) {
        this.cb.call(this.wv, IWebView2NavigationStartingEventArgs(pEventArgs))
        
        return 0 ; S_OK
    }
}

; We don't extend from IUnknown because the WebView actually implements this interface, not us.
class IWebView2NavigationStartingEventArgs {
    _uri := ""
    
    __New(pEventArgs) {
        this.pEventArgs := pEventArgs
    }
    
    Uri {
        get {
            if (!this._uri) {
                ComCall(3, this.pEventArgs, "Str*", &uri := "")
                this._uri := uri
            }
            
            return this._uri
        }
    }
    
    IsUserInitiated {
        get {
            ComCall(4, this.pEventArgs, "Int*", &isUserInitiated := 0)
            
            return isUserInitiated
        }
    }
    
    IsRedirected {
        get {
            ComCall(5, this.pEventArgs, "Int*", &isRedirected := 0)
            
            return isRedirected
        }
    }
    
    Cancel {
        get {
            ComCall(7, this.pEventArgs, "Int*", &isCanceled := 0)
            return isCanceled
        }
        
        set {
            ComCall(8, this.pEventArgs, "Int", Integer(value))
        }
    }
    
    navigationId {
        get {
            ComCall(9, this.pEventArgs, "Int64*", &id := 0)
            return id
        }
    }
}

class ICoreWebView2NavigationCompletedEventHandler extends IUnknown {
    __new(wv, cb) {
        super.__new()
        
        this.wv := wv
        this.cb := cb
    }
    
    Invoke(thisPtr, IWebView2WebView, pEventArgs) {
        this.cb.call(this.wv, IWebView2NavigationCompletedEventArgs(pEventArgs))
        
        return 0 ; S_OK
    }
}

class IWebView2NavigationCompletedEventArgs {
    static errors := ["UNKNOWN", "CERTIFICATE_COMMON_NAME_IS_INCORRECT", "CERTIFICATE_EXPIRED", "CLIENT_CERTIFICATE_CONTAINS_ERRORS", "CERTIFICATE_REVOKED", "CERTIFICATE_IS_INVALID", "SERVER_UNREACHABLE", "TIMEOUT", "ERROR_HTTP_INVALID_SERVER_RESPONSE", "CONNECTION_ABORTED", "CONNECTION_RESET", "DISCONNECTED", "CANNOT_CONNECT", "HOST_NAME_NOT_RESOLVED", "OPERATION_CANCELED", "REDIRECT_FAILED", "UNEXPECTED_ERROR"]

    __new(pEventArgs) {
        this.pEventArgs := pEventArgs
    }
    
    /**
     * @TODO: This doesn't appear to be working correctly.  It seems to return a random number instead of a BOOL
    */
    isSuccess {
        ; https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/0-9-538/icorewebview2navigationcompletedeventargs#get_issuccess
        get {
            ComCall(3, this.pEventArgs, "Int*", &isSuccess := 0)
            return isSuccess
        }
    }
    
    WebErrorStatus {
        ; Only valid if the navigation wasn't successful
        get {
            ComCall(4, this.pEventArgs, "Int*", &errorStatus := 0)
            return IWebView2NavigationCompletedEventArgs.errors[errorStatus + 1]
        }
    }
    
    navigationId {
        get {
            ComCall(5, this.pEventArgs, "Int64*", &id := 0)
            return id
        }
    }
}

class IUnknown {
    methods := ["QueryInterface", "AddRef", "Release", "Invoke"]
    vtbl := BufferAlloc(4 * A_PtrSize)
    
    __New() {
        for (name in this.methods) {
            NumPut("UPtr", CallbackCreate(this.%name%.bind(this), "", this.%name%.MinParams - 1), this.vtbl, (A_Index - 1) * A_PtrSize)
        }
        
        this.ptr := DllCall("Kernel32.dll\GlobalAlloc", "UInt", 0, "Ptr", A_PtrSize + 4, "UPtr")
        NumPut("UPtr", this.vtbl.ptr, this.ptr)
        NumPut("UInt", 1, this.ptr, A_PtrSize)
    }
    
    __Delete() {
        DllCall("Kernel32.dll\GlobalFree", "Ptr", this)
    }
    
    ; This isn't called for interfaces that we create and manage
    ; Explanation by Lexikos - https://www.autohotkey.com/boards/viewtopic.php?p=347465#p347465
    QueryInterface(riid, ppvObject) {
    }
    
    AddRef(interface) {
        ObjAddRef(interface)
    }
    
    Release(interface) {
        ObjRelease(interface)
    }
}